Skip to content

[M2-SCENE-01] Reference isometric scene generator (deterministic, seed-driven) - #93

Merged
offdev merged 2 commits into
masterfrom
feat/m2-scene-01
Oct 12, 2026
Merged

offdev merged 2 commits into
masterfrom
feat/m2-scene-01

Conversation

@offdev

@offdev offdev commented Oct 11, 2026

Copy link
Copy Markdown
Owner

M2-SCENE-01: Reference isometric scene generator

Implements roadmap step M2-SCENE-01 (roadmap/M2-rendering-2.5d.md, marked shipped): a headless, seed-deterministic generator for the worst-case isometric reference scene of PRD §4 — the scene every visual and performance acceptance test renders (downstream: M2-PERF-01, M2-AC-01, M2-GOLD-01, M2-SAMPLE-01).

What lands

  • New public header src/laige-render/include/laige/render/iso_reference_scene.h (header-only, no GL calls):
    • buildIsoReferenceScene(seed, variant) — the Full variant is the §8.1 budget scene: 50 000 sprites on a 256×256 stepped-terrain grid of uniform-height plateaus (each sprite standing on its PRNG-chosen tile; positions exact dyadic 2^-16 — no FMA-susceptible composition, ARCH-010), 3 preset-factor parallax layers, 8 continuous particle emitters, a 2-widget UI on the 1280×720 reference screen. The Small variant scales the same composition to 1 000 sprites / 32×32 for fast unit tests. Same seed → same bytes, every build and P0 platform.
    • A versioned little-endian binary scene format (v1): the reader treats the bytes as untrusted input (SCALE-004) — full domain validation of every field plus the exact total length; any violation fails the whole read with MalformedInput; the version gate rejects anything but 1 (ARCH-007).
    • writeIsoReferenceScene — atomic temp + rename (a killed run never leaves a partial file).
    • IsoReferenceSceneWorld<Backend> (move-only) + loadIsoReferenceScene (first failure wins) + declareIsoReferenceScene — the one-call whole-world pass (parallax + terrain tiles + 50k sprites with isoDepthKey depth keys + live particles), O(frame items), zero allocation. The scene's atlas plan pins 14 (atlas, material, blend) groups — under the §8.1 30-draw-call budget, asserted by the tests.
  • Headless tool tools/scene-iso/laige-scene-iso (--seed/--variant/--out): atomic write, round-trip self-check (byte-identical + clean re-parse), machine-greppable summary line, exit 0/2.
  • Tests (tests/laige-render/scene_iso_tests.cpp, ctest -R scene_iso): builder determinism (same-seed byte identity both variants, different-seed differ), the 7 malformed-input cases, round-trip deep equality, load + declare on both backends (per-batch instance counts, in-group depth-key monotonicity, the 14-group draw-call property), the UI pass (2 quads / 1 batch / 2 depth overrides), and the 1 000-frame zero-allocation declare loop (both backends).
  • Determinism gate scene_iso_cli (ctest): the tool twice per variant with the same seed, output files compared byte for byte — the roadmap Verify clause in executable form.
  • Fuzz fuzz_scene_iso_parse (1 000 bounded runs every commit, corpus = valid small + full scenes as mutate/truncate bases) for the untrusted-input parser surface (TEST-005 / SCALE-005).
  • Docs: docs/api/iso_reference_scene.md (API table, wire layout, Performance section, misuse warnings), docs/README.md index link, roadmap Shipped block. laige-api.json regenerated last (1815 symbols, 48 headers).

Engine bug fixed along the way

The scene loader's first use of the StringTable move hit a real bug: StringTable::operator=(StringTable&&) recursed into itself (*this = StringTable(std::move(o)) is again a move-assignment → infinite recursion, stack overflow). Fixed in src/laige-render/include/laige/render/text_items.h (member moves applied in place); regression test TextStringTable.MoveAssignment.

Notes

  • budgets.json unchanged (17 entries): the generation budget is the tool's (the scene_iso_cli 60 s timeout guards it), and the per-frame declare cost is part of the composite 50k render-CPU budget measured by M2-PERF-01.
  • GL-creating ctest entries were not run locally (GPU policy); they are CI-covered, as with prior steps.
  • Local Linux gate (all 4 trees, full ctest minus the GL entries): gate-gcc 115/115, gate-clang 115/115, gate-asan 112/112, gate-tsan 112/112; include-lint clean.

…d-driven)

The M2-SCENE-01 step: a headless, seed-deterministic generator for the
worst-case isometric reference scene of PRD §4 — the scene every
visual and performance acceptance test renders (M2-PERF-01,
M2-AC-01, M2-GOLD-01, M2-SAMPLE-01).

New public header src/laige-render/include/laige/render/iso_reference_scene.h
(header-only, no GL calls):

- buildIsoReferenceScene(seed, variant): the Full variant is the §8.1
  budget scene (50 000 sprites on a 256x256 stepped-terrain grid of
  uniform-height plateaus — each sprite standing on its PRNG-chosen
  tile, positions exact dyadic 2^-16), plus the 3 preset-factor
  parallax layers, 8 continuous particle emitters, and a 2-widget UI
  on the 1280x720 reference screen; the Small variant is the same
  composition scaled to 1 000 sprites / 32x32 for fast unit tests.
  Same seed, same bytes, every build and P0 platform (ARCH-010 —
  Prng substreams + the exact dyadic position scale, no
  FMA-susceptible float composition in the builder).
- The versioned little-endian binary scene format (v1): the reader
  treats the bytes as untrusted input (SCALE-004) — full domain
  validation of every field plus the exact total length; any
  violation fails the whole read with MalformedInput (ARCH-007 — the
  version gate rejects anything but 1).
- writeIsoReferenceScene: atomic temp + rename (a killed run never
  leaves a partial file).
- IsoReferenceSceneWorld<Backend> (move-only) + loadIsoReferenceScene
  (first failure wins; TileMap in unique_ptr — the house
  empty<=>stopped idiom, TileMap has no default constructor) +
  declareIsoReferenceScene: the one-call whole-world pass (parallax
  + terrain tiles + 50k sprites with their isoDepthKey depth keys +
  live particles), O(frame items), zero allocation. The scene's atlas
  plan pins 14 (atlas, material, blend) groups — under the §8.1
  30-draw-call budget, asserted by the test suite.

New headless tool tools/scene-iso/laige-scene-iso (the step's
generator CLI): --seed/--variant/--out, atomic write, round-trip
self-check (byte-identical + clean re-parse), machine-greppable
summary line, exit 0/2.

Verification:
- tests/laige-render/scene_iso_tests.cpp (ctest -R scene_iso): builder
  determinism (same-seed bytes identical, different-seed differ,
  both variants), the 7 malformed-input cases, round-trip deep
  equality, load + declare on both backends (per-batch instance
  counts, in-group depth-key monotonicity, the 14-group draw-call
  property), the UI pass (2 quads, 1 batch, 2 depth overrides), and
  the 1 000-frame zero-allocation declare loop (both backends).
- The scene_iso_cli ctest entry runs the roadmap's Verify clause in
  executable form: the tool twice per variant with the same seed,
  output files compared byte for byte.
- The scene format's untrusted-input surface gets a fuzz target
  (fuzz_scene_iso_parse, 1 000 bounded runs every commit, corpus =
  valid small + full scenes as mutate/truncate bases — TEST-005 /
  SCALE-005).

Also fixed (found by the scene loader's first use of the move):
StringTable::operator=(StringTable&&) recursed into itself
(`*this = StringTable(std::move(o))` is again a move-assignment —
infinite recursion). The move assignment now moves the members in
place; regression test TextStringTable.MoveAssignment.

docs/api/iso_reference_scene.md (the API + format + Performance
section), the docs/README.md index link, the roadmap Shipped block,
and laige-api.json regenerated last. budgets.json unchanged (17
entries — the generation budget is the tool's; the per-frame declare
cost is part of the composite 50k render-CPU budget, M2-PERF-01).
Unary minus applied to kIsoDepthLayerBias (a uint32 constant) before
the int32 cast warned C4146 on MSVC (treated as an error by the
house policy). Cast to int32 first, then negate — the iso_depth_key.h
clamp's own pattern (-static_cast to the signed type).
@offdev
offdev merged commit 2451424 into master Oct 12, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant